const fs = require('fs-extra');
const cluster = require('cluster');
const FileStreamRotator = require('file-stream-rotator');
const morgan = require('morgan');
const path = require('path');
const utility = require('./utility');
const ErrorLog = require('./mongo/error-log');
const cls = require('./cls');

const LogLevel = {
  verbose: 'VERBOSE',
  info: 'INFO',
  debug: 'DEBUG',
  error: 'ERROR',
  warn: 'WARN',
};
const LogLevelValue = {
  [LogLevel.verbose]: 1,
  [LogLevel.debug]: 2,
  [LogLevel.info]: 4,
  [LogLevel.warn]: 8,
  [LogLevel.error]: 16,
};
class Logger {
  constructor() {
    this.stream = process.stdout;
    this.logLevel = LogLevel.debug;
    if (process.env.NODE_ENV === 'production') {
      const { logsDir } = global.config.path;
      if (!fs.existsSync(logsDir)) {
        fs.mkdirSync(logsDir);
      }
      this.stream = FileStreamRotator.getStream({
        date_format: 'YYYY-MM-DD',
        filename: path.join(logsDir, 'access-%DATE%.log'),
        frequency: 'daily',
        verbose: false,
      });
    } else if (process.env.NODE_ENV === 'development') {
      this.logLevel = LogLevel.verbose;
    }
  }

  debug(...msg) {
    this.writeLog({
      level: LogLevel.debug,
      logCategory: Logger.getLogCategory().manual,
      msg: msg.join(' '),
    });
  }

  verbose(...msg) {
    this.writeLog({
      level: LogLevel.verbose,
      logCategory: Logger.getLogCategory().manual,
      msg: msg.join(' '),
    });
  }

  warn(...msg) {
    this.writeLog({
      level: LogLevel.warn,
      logCategory: Logger.getLogCategory().manual,
      msg: msg.join(' '),
    });
  }

  info(...msg) {
    this.writeLog({
      level: LogLevel.info,
      logCategory: Logger.getLogCategory().manual,
      msg: msg.join(' '),
    });
  }

  error(data, error) {
    if (utility.isObjectType(data)) {
      data.msg = data.msg || '';
    } else if (data === '' || utility.isStringType(data)) {
      data = { msg: data };
    } else {
      throw new Error('Invalid error data type.');
    }
    if (error instanceof Error) {
      data.msg += `${data.msg ? ' ' : ''}${error.message} ${error.stack}`;
    }
    this.writeLog({
      level: LogLevel.error,
      logCategory: Logger.getLogCategory().manual,
      ...data,
    });
    new ErrorLog({
      logTime: Math.floor(Date.now() / 1000),
      createdAt: new Date(),
      ...data,
    })
      .save()
      .catch(() => {});
  }

  httpLogFormat() {
    return morgan((tokens, req, res) => Logger.formatLog({
      level: LogLevel.info,
      httpMethod: tokens.method(req, res),
      httpPath: tokens.url(req, res),
      requestBodyParameters: req.body,
      statusCode: tokens.status(req, res),
      duration: tokens['response-time'](req, res),
      requestSize: tokens.res(req, res, 'content-length') || 0,
      logCategory: Logger.getLogCategory().http,
    }), { stream: this.stream });
  }

  writeLog(data) {
    if (LogLevelValue[data.level] >= LogLevelValue[this.logLevel]) {
      this.stream.write(`${Logger.formatLog(data)}\n`);
    }
  }

  static formatLog(data) {
    const session = cls.getRequestContext();
    if (session) {
      data.sessionId = session.id;
    }
    data.datetime = new Date().toISOString();
    if (process.env.NODE_ENV === 'development') {
      const workerRoleTag = cluster.isMaster ? 'MASTER' : `${process.workerRole} ${process.workerId}`;
      let str = `[${workerRoleTag}]${data.datetime} ${data.level}`;
      if (data.httpMethod && data.httpPath) {
        str += ` [${data.httpMethod} ${data.httpPath} ${data.duration}ms ${data.statusCode}]`;
      }
      if (data.msg) {
        str += ` ${data.msg}`;
      }
      return str;
    }
    return JSON.stringify(data);
  }

  static getLogCategory() {
    return {
      http: 'HttpServer',
      manual: 'Manual',
    };
  }
}

module.exports = new Logger();
